{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# PsychoPy experiments"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Parselmouth also allows Praat functionality to be included in an interactive [PsychoPy](https://www.psychopy.org/) experiment (refer to the subsection on [installing Parselmouth for PsychoPy](../installation.rst#psychopy) for detailed installation instructions for the PsychoPy graphical interface, the *PsychoPy Builder*). The following example shows how easily Python code that uses Parselmouth can be injected in such an experiment; following an adaptive staircase experimental design, at each trial of the experiment a new stimulus is generated based on the responses of the participant. See e.g. Kaernbach, C. (2001). Adaptive threshold estimation with unforced-choice tasks. *Attention, Perception, & Psychophysics*, *63*, 1377--1388., or the PsychoPy tutorial at https://www.psychopy.org/coder/tutorial2.html."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, we use an adaptive staircase experiment to determine the minimal amount of noise that makes the participant unable to distinguish between two audio fragments, *\"bat\"* and *\"bet\"* ([bat.wav](audio/bat.wav), [bet.wav](audio/bet.wav)). At every iteration of the experiment, we want to generate a version of these audio files with a specific signal-to-noise ratio, of course using Parselmouth to do so. Depending on whether the participant correctly identifies whether the noisy stimulus was *\"bat\"* or *\"bet\"*, the noise level is then either increased or decreased."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As Parselmouth is just another Python library, using it from the PsychoPy *Coder* interface or from a standard Python script that imports the `psychopy` module is quite straightforward. However, PsychoPy also features a so-called *Builder* interface, which is a graphical interface to set up experiments with minimal or no coding. In this *Builder*, a user can create multiple experimental *'routines'* out of different *'components'* and combine them through *'loops'*, that can all be configured graphically:\n",
    "\n",
    "![PsychoPy Builder interface](images/psychopy_builder_view.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For our simple example, we create a single routine `trial`, with a `Sound`, a `Keyboard`, and a `Text` component. We also insert a loop around this routine of the type `staircase`, such that PsychoPy will take care of the actual implementation of the loop in adaptive staircase design. The full PsychoPy experiment which can be opened in the *Builder* can be downloaded here: [adaptive_listening.psyexp](other/adaptive_listening.psyexp)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, to customize the behavior of the `trial` routine and to be able to use Parselmouth inside the PsychoPy experiment, we still add a `Code` component to the routine. This component will allow us to write Python code that interacts with the rest of the components and with the adaptive staircase loop. The `Code` components has different tabs, that allow us to insert custom code at different points during the execution of our trial."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First, there is the **Begin Experiment** tab. The code in this tab is executed only once, at the start of the experiment. We use this to set up the Python environment, importing modules and initializing variables, and defining constants:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ** Begin Experiment **\n",
    "\n",
    "import parselmouth\n",
    "import numpy as np\n",
    "import random\n",
    "\n",
    "conditions = ['a', 'e']\n",
    "stimulus_files = {'a': \"audio/bat.wav\", 'e': \"audio/bet.wav\"}\n",
    "\n",
    "STANDARD_INTENSITY = 70.\n",
    "stimuli = {}\n",
    "for condition in conditions:\n",
    "    stimulus = parselmouth.Sound(stimulus_files[condition])\n",
    "    stimulus.scale_intensity(STANDARD_INTENSITY)\n",
    "    stimuli[condition] = stimulus"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The code in the **Begin Routine** tab is executed before the routine, so in our example, for every iteration of the surrounding staircase loop. This allows us to actually use Parselmouth to generate the stimulus that should be played to the participant during this iteration of the routine. To do this, we need to access the current value of the adaptive staircase algorithm: PsychoPy stores this in the Python variable `level`. For example, at some point during the experiment, this could be 10 (representing a signal-to-noise ratio of 10 dB):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "level = 10"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To execute the code we want to put in the **Begin Routine** tab, we need to add a few variables that would be made available by the PsychoPy Builder, normally:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 'filename' variable is also set by PsychoPy and contains base file name of saved log/output files\n",
    "filename = \"data/participant_staircase_23032017\"\n",
    "\n",
    "# PsychoPy also create a Trials object, containing e.g. information about the current iteration of the loop\n",
    "# So let's quickly fake this, in this example, such that the code can be executed without errors\n",
    "# In PsychoPy this would be a `psychopy.data.TrialHandler` (https://www.psychopy.org/api/data.html#psychopy.data.TrialHandler)\n",
    "class MockTrials:\n",
    "    def addResponse(self, response):\n",
    "        print(\"Registering that this trial was {}successful\".format(\"\" if response else \"un\"))\n",
    "trials = MockTrials()\n",
    "trials.thisTrialN = 5 # We only need the 'thisTrialN' attribute of the 'trials' variable\n",
    "\n",
    "# The Sound component can also be accessed by it's name, so let's quickly mock that as well\n",
    "# In PsychoPy this would be a `psychopy.sound.Sound` (https://www.psychopy.org/api/sound.html#psychopy.sound.Sound)\n",
    "class MockSound:\n",
    "    def setSound(self, file_name):\n",
    "        print(\"Setting audio file of Sound component to '{}'\".format(file_name))\n",
    "sound_1 = MockSound()\n",
    "\n",
    "# And the same for our Keyboard component, `key_resp_2`:\n",
    "class MockKeyboard:\n",
    "    pass\n",
    "key_resp_2 = MockKeyboard()\n",
    "\n",
    "# Finally, let's also seed the random module to have a consistent output across different runs\n",
    "random.seed(42)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's also create the directory where we will store our example output\n",
    "!mkdir data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, we can execute the code that would be in the **Begin Routine** tab:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ** Begin Routine **\n",
    "\n",
    "random_condition = random.choice(conditions)\n",
    "random_stimulus = stimuli[random_condition]\n",
    "\n",
    "noise_samples = np.random.normal(size=random_stimulus.n_samples)\n",
    "noisy_stimulus = parselmouth.Sound(noise_samples,\n",
    "                     sampling_frequency=random_stimulus.sampling_frequency)\n",
    "noisy_stimulus.scale_intensity(STANDARD_INTENSITY - level)\n",
    "noisy_stimulus.values += random_stimulus.values\n",
    "noisy_stimulus.scale_intensity(STANDARD_INTENSITY)\n",
    "\n",
    "# use 'filename' to save our custom stimuli\n",
    "stimulus_file_name = filename + \"_stimulus_\" + str(trials.thisTrialN) + \".wav\"\n",
    "noisy_stimulus.resample(44100).save(stimulus_file_name, 'WAV')\n",
    "sound_1.setSound(stimulus_file_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's listen to the file we have just generated and that we would play to the participant:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import Audio\n",
    "Audio(filename=\"data/participant_staircase_23032017_stimulus_5.wav\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, we do not really need to have code executed during the trial (i.e., in the **Each Frame** tab). However, at the end of the trial, we need to inform the PsychoPy staircase loop whether the participant was correct or not, because this will affect the further execution the adaptive staircase, and thus value of the `level` variable set by PsychoPy. For this we add a final line in the **End Routine** tab. Let's say the participant guessed *\"bat\"* and pressed the `a` key:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "key_resp_2.keys = 'a'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The **End Routine** tab then contains the following code to check the participant's answer against the randomly chosen condition, and to inform the `trials` object of whether the participant was correct:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ** End Routine **\n",
    "\n",
    "trials.addResponse(key_resp_2.keys == random_condition)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Clean up the output directory again\n",
    "!rm -r data"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
